iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 23

[Day 23] useIntersectionObserver

  • 分享至 

  • xImage
  •  

官方 Demo:https://vueuse.org/core/useIntersectionObserver/#useintersectionobserver

IntersectionObserver Web API

這個原始碼的核心是 IntersectionObserver,主要可以透過這個 Web API 來得知目標元素是否出現在 root 的可視範圍中,以及在可視範圍中的狀態。因為是非同步執行,可以避免在主執行序上做偵測與計算。

參數部分提幾個等等會用到的:

  • callback:當目標元素可視區域時,要執行的 callback function。
  • options
    • root:指定根元素,預設值為瀏覽器 view port,以 Demo 為例,root 就是外層那個 scroll container。
    • rootMargin:類似 CSS margin,用於擴大或縮小根元素的有效範圍。以 Demo 為例,如果 rootMargin 設定 100px,那在元素進入可視區域之前就會被檢測到。
    • threshold:目標元素可視區域達到什麼比例時觸發 callback function。
      預設值為 0.1,也就是 target 的 10% 進入可視區域時會觸發 callback function。
      也可以是 array,例如傳入 [0, 0.5, 1] 的話,在 callback function 中拿到 entry.intersectionRatio 判斷當前的比例,可以做更細緻的效果。

細節可參考 mdn:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

useIntersectionObserver 參數

  • target:要偵測的目標元素,可以是 Array。
  • callback:同 IntersectionObserver Web API。
  • options
    • root:同 IntersectionObserver Web API。
    • rootMargin:同 IntersectionObserver Web API。
    • threshold:同 IntersectionObserver Web API。
    • window:瀏覽器環境預設為 window
    • immediate:是否要馬上呼叫 IntersectionObserver Web API 對 target 進行觀察。

useIntersectionObserver 原始碼

// src/compositions/useIntersectionObserver.js

import { computed, ref, watch } from 'vue'
import { defaultWindow, noop, notNullish, toValue, unrefElement } from '@/helper'
import { tryOnScopeDispose } from '@/utils/shared'
import { useSupported } from '@/compositions/useSupported'

export function useIntersectionObserver(target, callback, options = {}) {
  const {
    root,
    rootMargin = '0px',
    threshold = 0.1,
    window = defaultWindow,
    immediate = true,
  } = options

  const isSupported = useSupported(() => window && 'IntersectionObserver' in window)
  const targets = computed(() => {
    const _target = toValue(target)
    return (Array.isArray(_target) ? _target : [_target]).map(unrefElement).filter(notNullish)
  })

  let cleanup = noop
  const isActive = ref(immediate)

  const stopWatch = isSupported.value
    ? watch(
      () => [targets.value, unrefElement(root), isActive.value],
      ([targets, root]) => {
        cleanup()
        if (!isActive.value)
          return

        if (!targets.length)
          return

        const observer = new IntersectionObserver(
          callback,
          {
            root: unrefElement(root),
            rootMargin,
            threshold,
          },
        )

        targets.forEach(el => el && observer.observe(el))

        cleanup = () => {
          observer.disconnect()
          cleanup = noop
        }
      },
      { immediate, flush: 'post' },
    )
    : noop

  const stop = () => {
    cleanup()
    stopWatch()
    isActive.value = false
  }

  tryOnScopeDispose(stop)

  return {
    isSupported,
    isActive,
    pause() {
      cleanup()
      isActive.value = false
    },
    resume() {
      isActive.value = true
    },
    stop,
  }
}

這個 API 原始碼的結構跟之前 Day8 ~ Day10 看到的 useEventListener 有點相似,Day8 也有提到,在組件註銷前需要透過 tryOnScopeDispose(stop) 來 stopWatch 的部分,這裡就先不多談。

先看 isSupported,瀏覽器不支援 IntersectionObserver 的話就不用玩了 XD,到 caniuse 會發現主流瀏覽器都有支援,所以可以放心使用。

核心邏輯在 watch 內,來看看有哪些方式可以觸發這個 watch callback function。

  1. immediatetrue,組件掛載後馬上執行。
  2. immediatefalse,因為有 return resume 出去,在外層執行 resume 時,isActive 這個 ref 會變成 trueisActive 是這個 watch 觀察的物件,所以變化的時候會執行 watch callback function。

要如何暫停 IntersectionObserver API 的觀察?return 出去的物件中有一個 pause

pause() {
  cleanup()
  isActive.value = false
},

執行 cleanup 的時候,會透過 observer.disconnect() 來執行 IntersectionObserver API 的 disconnect 方法,這招就可以停止觀察。

接著來看核心:

// src/compositions/useIntersectionObserver.js
// ...略

const observer = new IntersectionObserver(
  callback,
  {
    root: unrefElement(root),
    rootMargin,
    threshold,
  },
)

targets.forEach(el => el && observer.observe(el))

// ...略

使用 IntersectionObserver Web API 來偵測 target(s) 在 root 元素中的狀態,target(s) 進到可視範圍中,觸發我們傳入的 callback
來看看 Demo code 怎麼使用 callback 的:

<!-- src/components/UseIntersectionObserverDemo.vue -->

<script setup>
// ...略

const { isActive, pause, resume } = useIntersectionObserver(
  target,
  ([{ isIntersecting }]) => {
    isVisible.value = isIntersecting
  },
  { root },
)
</script>

可以看到 callback 中透過解構拿到 IntersectionObserver Web API 給我們的 isIntersecting,isIntersecting 為 true 代表 target 進到可視範圍中。
callback function 的其他參數們可以參考 mdn:https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/isIntersecting

整個流程差不多就是這樣,看完覺得 IntersectionObserver Web API 真的是個厲害的角色,vueuse 也把它封裝成更方便使用的版本,如果要做更細緻的功能,也可以透過 callback function 拿到更多 IntersectionObserver Web API 參數來處理,滿喜歡這種設計。

GitHub PR:https://github.com/RhinoLee/30days_vue/pull/22/files


useIntersectionObserver API 就到這邊告一段落啦~
明天繼續看 useElementVisibility API,其實本來今天就要看了 XD 只是突然發現原來 useElementVisibility 的核心是 useIntersectionObserver。


上一篇
[Day 22] useScroll - scrolling
下一篇
[Day 24] useElementVisibility
系列文
30 天 vueuse 原始碼閱讀與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言